# ParameterBinderRegistry.java

    package org.codefilarete.stalactite.sql.statement.binder;

    import java.io.File;
    import java.io.InputStream;
    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.nio.file.Path;
    import java.sql.Blob;
    import java.time.Instant;
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    import java.time.ZoneId;
    import java.util.Date;
    import java.util.Iterator;
    import java.util.Map;
    import java.util.Objects;
    import java.util.Set;
    import java.util.UUID;
    import java.util.WeakHashMap;
    import java.util.function.Function;

    import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
    import org.codefilarete.tool.Duo;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.bean.ClassIterator;
    import org.codefilarete.tool.bean.InterfaceIterator;
    import org.codefilarete.tool.collection.Arrays;
    import org.codefilarete.tool.collection.Iterables;

    import static java.util.stream.Collectors.toSet;

    /**
     * Registry of {@link ParameterBinder}s according to their binding class.
     * <br>
     * Design note : this class can't implement {@link ParameterBinderIndex} because it already defines
     * {@link #getBinder(Class)} with generified Class type. This disallows to have a none generified version of it, which
     * is the default given while implementing {@link ParameterBinderIndex}&lt;Class&gt;. Moreover, it would disallow also
     * children classes to implement another type of key as index since classes can't implement twice an interface with 2
     * different generic types due to type erasure.
     *
     * @author Guillaume Mary
     */
    public class ParameterBinderRegistry {

    	public enum EnumBindType {

    		NAME(NameEnumParameterBinder::new),
    		ORDINAL(OrdinalEnumParameterBinder::new);

    		private final Function<Class<? extends Enum>, AbstractEnumParameterBinder> factory;

    		EnumBindType(Function<Class<? extends Enum>, AbstractEnumParameterBinder> factory) {
    			this.factory = factory;
    		}

    		public <E extends Enum<E>> ParameterBinder<E> newParameterBinder(Class<E> enumType) {
    			return new NullAwareParameterBinder<>(factory.apply(enumType));
    		}
    	}

    	private final WeakHashMap<Class<?>, ParameterBinder<?>> binderPerType = new WeakHashMap<>();

    	public ParameterBinderRegistry() {
    		registerParameterBinders();
    	}

    	public Map<Class<?>, ParameterBinder<?>> getBinderPerType() {
    		return binderPerType;
    	}

    	public <T> void register(Class<T> clazz, ParameterBinder<T> parameterBinder) {
    		binderPerType.put(clazz, parameterBinder);
    	}

    	protected void registerParameterBinders() {
    		// Note that enum types are registered dynamically
    		register(String.class, DefaultParameterBinders.STRING_BINDER);
    		register(Double.class, DefaultParameterBinders.DOUBLE_BINDER);
    		register(Double.TYPE, DefaultParameterBinders.DOUBLE_PRIMITIVE_BINDER);
    		register(Number.class, DefaultParameterBinders.NUMBER_BINDER);
    		register(Float.class, DefaultParameterBinders.FLOAT_BINDER);
    		register(Float.TYPE, DefaultParameterBinders.FLOAT_PRIMITIVE_BINDER);
    		register(BigDecimal.class, DefaultParameterBinders.BIGDECIMAL_BINDER);
    		register(BigInteger.class, new NullAwareParameterBinder<>(new LambdaParameterBinder<>(DefaultParameterBinders.LONG_PRIMITIVE_BINDER, BigInteger::valueOf, BigInteger::longValue)));
    		register(Long.class, DefaultParameterBinders.LONG_BINDER);
    		register(Long.TYPE, DefaultParameterBinders.LONG_PRIMITIVE_BINDER);
    		register(Integer.class, DefaultParameterBinders.INTEGER_BINDER);
    		register(Integer.TYPE, DefaultParameterBinders.INTEGER_PRIMITIVE_BINDER);
    		register(Byte.class, DefaultParameterBinders.BYTE_BINDER);
    		register(Byte.TYPE, DefaultParameterBinders.BYTE_PRIMITIVE_BINDER);
    		register(byte[].class, DefaultParameterBinders.BYTES_BINDER);
    		register(Date.class, DefaultParameterBinders.DATE_BINDER);
    		register(java.sql.Date.class, DefaultParameterBinders.DATE_SQL_BINDER);
    		register(LocalDate.class, DefaultParameterBinders.LOCALDATE_BINDER);
    		register(LocalDateTime.class, DefaultParameterBinders.LOCALDATETIME_BINDER);
    		register(LocalTime.class, DefaultParameterBinders.LOCALTIME_BINDER);
    		register(Instant.class, new LambdaParameterBinder<>(DefaultParameterBinders.LONG_BINDER, Instant::ofEpochMilli, Instant::toEpochMilli));
    		register(java.sql.Timestamp.class, DefaultParameterBinders.TIMESTAMP_BINDER);
    		register(Boolean.class, DefaultParameterBinders.BOOLEAN_BINDER);
    		register(Boolean.TYPE, DefaultParameterBinders.BOOLEAN_PRIMITIVE_BINDER);
    		register(InputStream.class, DefaultParameterBinders.BINARYSTREAM_BINDER);
    		register(Blob.class, DefaultParameterBinders.BLOB_BINDER);
    		register(ZoneId.class, DefaultParameterBinders.ZONEID_BINDER);
    		register(UUID.class, DefaultParameterBinders.UUID_BINDER);
    		register(Path.class, DefaultParameterBinders.PATH_BINDER);
    		register(File.class, DefaultParameterBinders.FILE_BINDER);
    	}

    	/**
    	 * Gives the registered {@link ParameterBinder} for the given type.
    	 *
    	 * @param clazz a class
    	 * @return the registered {@link ParameterBinder} for the given type
    	 * @throws BindingException if the binder doesn't exist
    	 */
    	public <T> ParameterBinder<T> getBinder(Class<T> clazz) {
    		ParameterBinder<T> parameterBinder = (ParameterBinder<T>) binderPerType.get(clazz);
    		if (parameterBinder == null) {
    			parameterBinder = lookupForBinder(clazz);
    		}
    		return parameterBinder;
    	}

    	private <T> Set<Duo<Class<?>, ParameterBinder<?>>> lookupForCompatibleBinder(Class<T> clazz) {
    		if (clazz.isEnum()) {
    			return Arrays.asSet(new Duo<>(clazz, binderPerType.computeIfAbsent(clazz, k -> new NullAwareParameterBinder<>(new OrdinalEnumParameterBinder(k)))));
    		}

    		Iterator<Class<?>> classHierarchy = new ClassIterator(clazz);
    		return Iterables.stream(classHierarchy)
    				.map(classAncestor -> {
    					ParameterBinder<?> parameterBinder = binderPerType.get(classAncestor);
    					Duo<Class<?>, ParameterBinder<?>> classParameterBinderDuo = new Duo<>(clazz, parameterBinder);
    					return parameterBinder == null ? null : (Set<Duo<Class<?>, ParameterBinder<?>>>) Arrays.asSet(classParameterBinderDuo);
    				})
    				// we keep only class for which a binder exists
    				.filter(Objects::nonNull)
    				.findFirst()
    				.orElseGet(() -> {
    					// if no binder was found during class hierarchy scan, we scan the interfaces
    					Set<Duo<? extends Class<?>, ? extends ParameterBinder<?>>> result = Iterables.stream(new InterfaceIterator(clazz))
    							.map(pawn -> new Duo<>(pawn, binderPerType.get(pawn)))
    							// we keep only class for which a binder exists
    							.filter(duo -> duo.getRight() != null)
    							.collect(toSet());
    					return (Set<Duo<Class<?>, ParameterBinder<?>>>) (Set) result;
    				});
    	}

    	private <T> ParameterBinder<T> lookupForBinder(Class<T> clazz) {
    		Set<Duo<Class<?>, ParameterBinder<?>>> binderPerInterface = lookupForCompatibleBinder(clazz);
    		if (binderPerInterface.size() > 1) {
    			throw new BindingException("Multiple binders found for " + Reflections.toString(clazz)
    					+ ", please register one for any of : " + Iterables.stream(binderPerInterface).map(Duo::getLeft).map(Reflections::toString).collect(toSet()));
    		} else if (binderPerInterface.isEmpty()) {
    			throw new BindingException("No binder found for type " + Reflections.toString(clazz));
    		}
    		ParameterBinder<T> foundBinder = (ParameterBinder<T>) Iterables.first(binderPerInterface).getRight();
    		// we put the found binder to save computation of a next call (optional action)
    		binderPerType.put(clazz, foundBinder);
    		return foundBinder;
    	}
    }